<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="Dprpt" id="Dprpt"><span data-lake-id="u23f67617" id="u23f67617">典型回答</span></h1>
  <p data-lake-id="ue9a6f377" id="ue9a6f377"><br></p>
  <p data-lake-id="ub52f32f0" id="ub52f32f0"><span data-lake-id="u28b6921d" id="u28b6921d">在操作系统中，零拷贝指的是避免在用户态（User-space）与内核态（Kernel-space）之间来回拷贝数据。</span></p>
  <p data-lake-id="ua45b8d87" id="ua45b8d87"><span data-lake-id="u8c0a9efc" id="u8c0a9efc">​</span><br></p>
  <p data-lake-id="u4078c0a8" id="u4078c0a8"><br></p>
  <p data-lake-id="u6b5ac93c" id="u6b5ac93c"><span data-lake-id="u53795102" id="u53795102">而Netty的零拷贝模型和操作系统中的零拷贝模型并不完全一样。他主要指的是在操作数据时, 不需要将数据 buffer从 一个内存区域拷贝到另一个内存区域。少了一次内存的拷贝，CPU 效率就得到的提升。</span></p>
  <p data-lake-id="ue0e8f340" id="ue0e8f340"><span data-lake-id="u7c196854" id="u7c196854">​</span><br></p>
  <p data-lake-id="uf4e45534" id="uf4e45534"><span data-lake-id="u0f681590" id="u0f681590">Netty的零拷贝主要体现在以下 5 个方面：</span></p>
  <ul list="u542af511">
   <li fid="u2e7dcc46" data-lake-id="u35531451" id="u35531451"><strong><span data-lake-id="uccfab295" id="uccfab295">直接使用堆外内存</span></strong><span data-lake-id="u35b52c5b" id="u35b52c5b">，避免 JVM 堆内存到堆外内存的数据拷贝。</span></li>
   <li fid="u2e7dcc46" data-lake-id="u8221939a" id="u8221939a"><strong><span data-lake-id="u8f443531" id="u8f443531">CompositeByteBuf 类</span></strong><span data-lake-id="uf14cbcb6" id="uf14cbcb6">，可以组合多个 Buffer 对象合并成一个逻辑上的对象，避免通过传统内存拷贝的方式将几个 Buffer 合并成一个大的 Buffer。</span></li>
   <li fid="u2e7dcc46" data-lake-id="u53770dc3" id="u53770dc3"><span data-lake-id="u9415a360" id="u9415a360">通过</span><strong><span data-lake-id="u2dfe14d0" id="u2dfe14d0"> Unpooled.wrappedBuffer</span></strong><span data-lake-id="ua9b40f99" id="ua9b40f99"> 可以将 byte 数组包装成 ByteBuf 对象，包装过程中不会产生内存拷贝。</span></li>
   <li fid="u2e7dcc46" data-lake-id="uc1964a99" id="uc1964a99"><strong><span data-lake-id="u7be34826" id="u7be34826">ByteBuf.slice</span></strong><span data-lake-id="u44215da3" id="u44215da3"> 操作与 Unpooled.wrappedBuffer 相反，slice 操作可以将一个 ByteBuf 对象切分成多个 ByteBuf 对象，切分过程中不会产生内存拷贝，底层共享一个 byte 数组的存储空间。</span></li>
   <li fid="u2e7dcc46" data-lake-id="u2c6b21a0" id="u2c6b21a0"><strong><span data-lake-id="u25fc9dcc" id="u25fc9dcc">使用 FileRegion 实现文件传输</span></strong><span data-lake-id="u73497d5b" id="u73497d5b">，FileRegion 底层封装了 FileChannel#transferTo() 方法，可以将文件缓冲区的数据直接传输到目标 Channel，避免内核缓冲区和用户态缓冲区之间的数据拷贝，这属于操作系统级别的零拷贝。</span></li>
  </ul>
  <p data-lake-id="ua55f4c1c" id="ua55f4c1c"><span data-lake-id="u92229ee9" id="u92229ee9">​</span><br></p>
  <h1 data-lake-id="HYL3c" id="HYL3c"><span data-lake-id="u4591ed69" id="u4591ed69">扩展知识</span></h1>
  <h2 data-lake-id="Edd4K" id="Edd4K"><span data-lake-id="ud6267bfa" id="ud6267bfa">堆外内存</span></h2>
  <p data-lake-id="ueca2e708" id="ueca2e708"><span data-lake-id="u93ae82dc" id="u93ae82dc">我们都知道。Java在将数据发送出去的时候，会先将数据从堆内存拷贝到堆外内存，然后才会将堆外内存再拷贝到内核态，进行消息的收发，代码如下：</span></p>
  <p data-lake-id="u6cbe454e" id="u6cbe454e"><span data-lake-id="udd4bdc08" id="udd4bdc08">​</span><br></p>
  <pre lang="java"><code>
static int write(FileDescriptor paramFileDescriptor, 
                 ByteBuffer paramByteBuffer, long paramLong, 
                 NativeDispatcher paramNativeDispatcher) throws IOException{
	// 如果是直接内存，则直接写入
    if((paramByteBuffer instanceof DirectBuffer)) {
        return writeFromNativeBuffer(paramFileDescriptor, paramByteBuffer, paramLong, paramNativeDispatcher);
    }
    // ...否则，先把数据拷贝到直接内存中
    ByteBuffer localByteBuffer = Util.getTemporaryDirectBuffer(k);
    try {
        localByteBuffer.put(paramByteBuffer);
        localByteBuffer.filp();
        paramByteBuffer.position(i);
        int m = writeFromNativeBuffer(paramFileDescriptor, localByteBuffer, paramLong, paramNativeDispatcher);
    }
}
</code></pre>
  <p data-lake-id="u87d278b4" id="u87d278b4"><span data-lake-id="u20b86e9e" id="u20b86e9e">​</span><br></p>
  <p data-lake-id="u7c110c6a" id="u7c110c6a"><span data-lake-id="u2702d7b9" id="u2702d7b9">所以，我们发现，假如我们在收发报文的时候使用直接内存，那么就可以减少一次内存拷贝，Netty就是这么做的。</span></p>
  <p data-lake-id="u78685e8f" id="u78685e8f"><span data-lake-id="u330c7fca" id="u330c7fca">​</span><br></p>
  <p data-lake-id="u0ed771ef" id="u0ed771ef"><span data-lake-id="u24645850" id="u24645850">Netty在通信层进行字节流的接收和发送的时候，如果应用允许Unsafe访问，则会采用DirectByteBuf进行转换，也就是堆外的直接内存，代码如下：</span></p>
  <p data-lake-id="uce5f1698" id="uce5f1698"><span data-lake-id="u5c5c14c0" id="u5c5c14c0">​</span><br></p>
  <pre lang="java"><code>
public ByteBuf ioBuffer(int initialCapacity) {
    if (PlatformDependent.hasUnsafe() || isDirectBufferPooled()) {
    return directBuffer(initialCapacity);
}
return heapBuffer(initialCapacity);
}
</code></pre>
  <p data-lake-id="u471e8487" id="u471e8487"><br></p>
  <h2 data-lake-id="FBaDe" id="FBaDe"><span data-lake-id="u1c868cb0" id="u1c868cb0">CompositeByteBuf</span></h2>
  <p data-lake-id="uc2941da3" id="uc2941da3"><br></p>
  <p data-lake-id="u289e983f" id="u289e983f"><span data-lake-id="u7b55768f" id="u7b55768f">考虑一种场景，当一个数据包被拆成了两个字节流通过TCP传输过来后，那么对于接收者的机器来说，为了方便解析，它需要新建一个ByteBuf将这两个字节流重组成一个新的数据包，如下图所示：</span></p>
  <p data-lake-id="u8f026254" id="u8f026254"><img src="https://cdn.nlark.com/yuque/0/2023/png/719664/1673186575978-cb175194-a745-4ce2-a617-1e1beeb75e3e.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_12%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="u984967eb" id="u984967eb"><span data-lake-id="u1581247b" id="u1581247b">那么在这种情况下，我们如果直接将两个字节流拷贝到一个新的字节流中，显然会浪费空间和时间，所以Netty推出了CompositeByteBuf，专门用来拷贝ByteBuf</span></p>
  <p data-lake-id="ua0c48539" id="ua0c48539"><img src="https://cdn.nlark.com/yuque/0/2023/png/719664/1673186297380-7ba2e802-0d0d-444c-b9af-f987e330ca9e.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_28%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="uc4e901a9" id="uc4e901a9"><span data-lake-id="u246c84f3" id="u246c84f3">从图中可以看到，实际的Buf还是两个，只不过Netty通过CompositeByteBuf将老的buf通过指针组合映射到新的Buf中，减少了一次拷贝过程。</span></p>
  <p data-lake-id="u7b307073" id="u7b307073"><br></p>
  <h2 data-lake-id="Z7CMs" id="Z7CMs"><span data-lake-id="ua38b09ef" id="ua38b09ef">Unpooled.wrappedBuffer</span></h2>
  <p data-lake-id="u0d2a182a" id="u0d2a182a"><br></p>
  <p data-lake-id="u7667a540" id="u7667a540"><span data-lake-id="uf63b56b4" id="uf63b56b4">Unpooled.wrappedBuffer 是创建 CompositeByteBuf 对象的另一种推荐做法。</span></p>
  <p data-lake-id="u90516ef6" id="u90516ef6"><br></p>
  <p data-lake-id="ue69361b2" id="ue69361b2"><span data-lake-id="uaa6a4d0b" id="uaa6a4d0b">Unpooled.wrappedBuffer 方法可以将不同的数据源的一个或者多个数据包装成一个大的 ByteBuf 对象，其中数据源的类型包括 byte[]、ByteBuf、ByteBuffer。包装的过程中不会发生数据拷贝操作，包装后生成的 ByteBuf 对象和原始 ByteBuf 对象是共享底层的 byte 数组。</span></p>
  <p data-lake-id="u4682ea2e" id="u4682ea2e"><span data-lake-id="uf172d0ac" id="uf172d0ac">​</span><br></p>
  <h2 data-lake-id="sJ6G0" id="sJ6G0"><span data-lake-id="u6e4125f9" id="u6e4125f9">ByteBuf.slice</span></h2>
  <p data-lake-id="u92ca0831" id="u92ca0831"><span data-lake-id="u38016afa" id="u38016afa">ByteBuf.slice 和 Unpooled.wrappedBuffer 的逻辑正好相反，ByteBuf.slice 是将一个 ByteBuf 对象切分成多个共享同一个底层存储的 ByteBuf 对象。<br><br></span></p>
  <h2 data-lake-id="cptt8" id="cptt8"><span data-lake-id="ua66fa05a" id="ua66fa05a">FileRegion文件传输</span></h2>
  <p data-lake-id="uf56b12a9" id="uf56b12a9"><span data-lake-id="uaceed5c5" id="uaceed5c5" class="lake-fontsize-12" style="color: rgb(59, 67, 81)">Netty 使用 FileRegion 实现文件传输的零拷贝，而FileRegion其实是基于</span><span data-lake-id="u07a677d4" id="u07a677d4">Java底层的FileChannel#tranferTo方法实现的。它可以根据操作系统直接将文件缓冲区的数据发送到目标channel，底层借助了sendFile能力避免了传统通过循环write方式导致的内存拷贝问题。所以 FileRegion 是操作系统级别的零拷贝。</span></p>
  <p data-lake-id="u5220edec" id="u5220edec"><span data-lake-id="ucd17b6de" id="ucd17b6de">​</span><br></p>
  <p data-lake-id="u8c8ee649" id="u8c8ee649"><span data-lake-id="u91462474" id="u91462474">JavaDoc的注释如下：</span></p>
  <blockquote data-lake-id="ub68f2670" id="ub68f2670">
   <p data-lake-id="u96600a93" id="u96600a93"><span data-lake-id="u219465ec" id="u219465ec">This method is potentially much more efficient than a simple loop that reads from this channel and writes to the target channel. </span><strong><span data-lake-id="u9f718eb2" id="u9f718eb2">Many operating systems can transfer bytes directly from the filesystem cache to the target channel without actually copying them.</span></strong></p>
   <p data-lake-id="udcbf92c1" id="udcbf92c1"><strong><span data-lake-id="uf096eafb" id="uf096eafb">​</span></strong><br></p>
  </blockquote>
  <h3 data-lake-id="ZQA80" id="ZQA80"><span data-lake-id="ub8820629" id="ub8820629">sendFile</span></h3>
  <p data-lake-id="u1a32829c" id="u1a32829c"><span data-lake-id="u78e00217" id="u78e00217">JDK原生的FileChannel#tranferTo方法其实是基于了Linux的sendFile方法，通过该方法，数据可以直接在内核空间内部进行 I/O 传输，从而省去了数据在用户空间和内核空间之间的来回拷贝。工作原理如下图：</span></p>
  <p data-lake-id="ue4f0e7d7" id="ue4f0e7d7"><span data-lake-id="u6abaa3a9" id="u6abaa3a9">​</span><br></p>
  <p data-lake-id="ue701aeff" id="ue701aeff"><img src="https://cdn.nlark.com/yuque/0/2023/png/719664/1673266150227-a16e0d98-9fda-47c4-ad81-c795d391dcf6.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_27%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="u812b8c5e" id="u812b8c5e"><span data-lake-id="u42672bfa" id="u42672bfa">​</span><br></p>
 </body>
</html>